use crate::{asset_info::AssetInfo, common::read_lines_range};
use anyhow::Result;
use base64::{engine::general_purpose, Engine};
use regex::Regex;
use std::{
    borrow::Cow,
    fs::File,
    io::{BufRead, BufReader, Read, Seek, SeekFrom, Write},
    path::Path,
    time::{SystemTime, UNIX_EPOCH},
    vec,
};
use urlencoding::encode;

pub struct OebUnpack {
    path: String,
    assets: Vec<AssetInfo>,
    start_content_id: Option<String>,
}

impl Drop for OebUnpack {
    fn drop(&mut self) {
        self.close();
    }
}

impl OebUnpack {
    pub fn new(path: &str) -> Self {
        Self {
            assets: vec![],
            path: path.to_string(),
            start_content_id: None,
        }
    }

    pub fn analysis(&mut self) -> Result<Vec<AssetInfo>> {
        if self.assets.len() == 0 {
            let mut file = File::open(&self.path)?;
            let target = b"application/x-oeb1";
            let mut buffer = [0u8; 128];
            let bytes_read = file.read(&mut buffer)?;
            let is_oeb = buffer[..bytes_read]
                .windows(target.len())
                .any(|window| window == target);
            file.seek(SeekFrom::Start(0))?;
            if is_oeb {
                let reader = BufReader::new(file);
                let re = Regex::new(r#"(\w+)="([^"]*)""#)?;
                let href_re = Regex::new(r#"href="([^"]+)""#)?;
                let mut boundary = String::from("");
                let mut asset = AssetInfo::default();
                for (index, line) in reader.lines().enumerate() {
                    let text = line?;
                    if boundary.trim().len() == 0 {
                        for (_, [key, value]) in re.captures_iter(&text).map(|c| c.extract()) {
                            match key.trim().to_lowercase().as_str() {
                                "boundary" => boundary = value.trim().to_string(),
                                "start" => self.start_content_id = Some(value.trim().to_string()),
                                _ => {}
                            }
                        }
                    } else {
                        if text.trim_matches('-') == &boundary {
                            let num = index as u16;
                            if asset.start == 0 {
                                asset.start = num + 2;
                            } else {
                                asset.end = num;
                                self.assets.push(asset);
                                asset = AssetInfo::default();
                                asset.start = num + 2;
                            }
                        } else {
                            if text.to_lowercase().starts_with("content-") {
                                let keys: Vec<&str> = text.split(':').collect();
                                match keys[0].to_lowercase().trim() {
                                    "content-type" => {
                                        asset.content_type = keys[1].trim().to_string()
                                    }
                                    "content-disposition" => {
                                        let value = keys[1].trim().to_string();
                                        let result = href_re
                                            .captures(&value)
                                            .and_then(|cap| cap.get(1))
                                            .map_or("", |m| m.as_str());
                                        asset.content_disposition = result.trim().to_string();
                                    }
                                    "content-oeb-id" | "content-id" => {
                                        asset.content_id = keys[1].trim().to_string()
                                    }
                                    "content-transfer-encoding" => {
                                        asset.content_encoding = keys[1].trim().to_string()
                                    }
                                    _ => {}
                                }
                            }
                        }
                    }
                }
            }
        }
        Ok(self.assets.to_vec())
    }

    pub fn start_content_id(&mut self) -> String {
        if let Some(value) = &self.start_content_id {
            value.to_string()
        } else {
            String::default()
        }
    }

    pub fn save_to(&mut self, assets_id: &str, folder_path: &str) -> Result<Option<String>> {
        let mut success = None;
        if let Some(assets) = self
            .assets
            .iter_mut()
            .find(|f| f.content_id.eq_ignore_ascii_case(&assets_id))
        {
            let content = read_lines_range(&self.path, assets.start, assets.end);
            let mut content_disposition = encode(&assets.content_disposition);
            if content_disposition.trim().len() == 0 {
                let mut ext = "";
                let since_the_epoch = SystemTime::now().duration_since(UNIX_EPOCH)?;
                let ms = since_the_epoch.as_secs() * 1000 + since_the_epoch.subsec_millis() as u64;
                if assets.content_type.eq_ignore_ascii_case("text/xml") {
                    ext = ".xml";
                }
                content_disposition = Cow::from(format!("{}{}", ms, ext));
            }
            let file_path = Path::new(&folder_path).join(content_disposition.trim());
            let mut file = File::create(&file_path)?;
            if assets
                .content_encoding
                .trim()
                .eq_ignore_ascii_case("base64")
            {
                let buf = general_purpose::STANDARD.decode(&content)?;
                file.write_all(&buf)?;
                success = Some(file_path.to_str().unwrap_or_default().to_string());
            } else {
                let buf = content.as_bytes();
                file.write_all(&buf)?;
                success = Some(file_path.to_str().unwrap_or_default().to_string());
            }
        }
        Ok(success)
    }

    pub fn close(&mut self) {
        self.assets.clear();
        self.assets = vec![];
    }
}

#[test]
fn test_unpack() {
    let mut instance = OebUnpack::new("test.oeb");
    if let Ok(assets) = instance.analysis() {
        assert!(assets.len() > 0);
        for asset in assets {
            let path = instance.save_to(&asset.content_id, "").unwrap();
            assert!(path.is_some());
        }
    } else {
        assert!(false);
    }
    instance.close();
}
